description: Internal architecture of go-ansible -- key types, data flow, module dispatch, templating, and SSH transport.
---
# Architecture
This document explains how `go-ansible` works internally. The package is a single flat Go package (`package ansible`) with four distinct layers: **types**, **parser**, **executor**, and **SSH transport**.
1. The **Parser** reads YAML files and produces typed Go structs (`Play`, `Task`, `Inventory`).
2. The **Executor** iterates over plays and tasks, resolving hosts from inventory, evaluating conditions, expanding templates, and dispatching each task to the appropriate module handler.
A `Task` is the fundamental unit of work. The `Module` and `Args` fields are not part of the YAML schema directly -- they are extracted at parse time by custom `UnmarshalYAML` logic that scans for known module keys:
```go
type Task struct {
Name string // Human-readable task name
Module string // Derived: module name (e.g. "apt", "shell")
Args map[string]any // Derived: module arguments
Register string // Variable name to store result
When any // Condition (string or []string)
Loop any // Items to iterate ([]any or var reference)
Data map[string]any // Module-specific structured data
Duration time.Duration // Execution time
}
```
Results can be captured via `register:` and referenced in later `when:` conditions or `{{ var.stdout }}` templates.
### Inventory
```go
type Inventory struct {
All *InventoryGroup
}
type InventoryGroup struct {
Hosts map[string]*Host
Children map[string]*InventoryGroup
Vars map[string]any
}
type Host struct {
AnsibleHost string
AnsiblePort int
AnsibleUser string
AnsiblePassword string
AnsibleSSHPrivateKeyFile string
AnsibleConnection string
AnsibleBecomePassword string
Vars map[string]any // Custom vars via inline YAML
}
```
Host resolution supports the `all` pattern, group names, and individual host names. The `GetHosts()` function performs pattern matching, and `GetHostVars()` collects variables by walking the group hierarchy (group vars are inherited, host vars take precedence).
### Facts
The `Facts` struct holds basic system information gathered from remote hosts via shell commands:
```go
type Facts struct {
Hostname string
FQDN string
Distribution string // e.g. "ubuntu"
Version string // e.g. "24.04"
Architecture string // e.g. "x86_64"
Kernel string // e.g. "6.8.0"
// ...
}
```
Facts are gathered automatically at the start of each play (unless `gather_facts: false`) and are available for templating via `{{ ansible_hostname }}`, `{{ ansible_distribution }}`, etc.
## Parser
The parser (`parser.go`) handles four types of YAML file:
Unresolved expressions are returned verbatim (e.g. `{{ undefined_var }}` remains as-is).
Template file processing (`TemplateFile`) performs a basic Jinja2-to-Go-template conversion for the `template` module, with a fallback to simple `{{ }}` substitution.
### Loops
The executor supports `loop:` (and the legacy `with_items:`) with configurable loop variable names via `loop_control`. Loop results are aggregated into a single `TaskResult` with a `Results` slice.
### Block / Rescue / Always
Error handling blocks follow Ansible semantics:
1. Execute all tasks in `block`.
2. If any `block` task fails and `rescue` is defined, execute the rescue tasks.
3. Always execute `always` tasks regardless of success or failure.
### Handler Notification
When a task produces `Changed: true` and has a `notify` field, the named handler(s) are marked. After all tasks in the play complete, notified handlers are executed in the order they are defined.
## Module Dispatch
The `executeModule` method in `modules.go` normalises the module name (adding the `ansible.builtin.` prefix if absent) and dispatches to the appropriate handler via a `switch` statement.
Each module handler:
1. Extracts arguments using helper functions (`getStringArg`, `getBoolArg`).
2. Constructs one or more shell commands.
3. Executes them via the SSH client.
4. Parses the output into a `TaskResult`.
Some modules (e.g. `debug`, `set_fact`, `fail`, `assert`) are purely local and do not require SSH.
These handle type coercion (e.g. `"yes"`, `"true"`, `"1"` all evaluate to `true` for `getBoolArg`).
## SSH Transport
The `SSHClient` (`ssh.go`) manages connections to remote hosts.
### Authentication
Authentication methods are tried in order:
1.**Explicit key file** -- from `ansible_ssh_private_key_file` or the `KeyFile` config field.
2.**Default keys** -- `~/.ssh/id_ed25519`, then `~/.ssh/id_rsa`.
3.**Password** -- both `ssh.Password` and `ssh.KeyboardInteractive` are registered.
### Host Key Verification
The client uses `~/.ssh/known_hosts` for host key verification via `golang.org/x/crypto/ssh/knownhosts`. If the file does not exist, it is created automatically.
### Privilege Escalation (become)
When `become` is enabled (at play or task level), commands are wrapped with `sudo`:
File uploads use `cat >` piped via an SSH session's stdin rather than SCP. This approach is simpler and works well with `become`, as the `sudo` wrapper can be applied to the `cat` command. Downloads use `cat` with quoted paths.
### Connection Lifecycle
SSH connections are created lazily (on first use per host) and cached in the executor's `clients` map. The `Executor.Close()` method terminates all open connections.
## Concurrency
The executor uses a `sync.RWMutex` to protect shared state (variables, results, SSH client cache). Tasks within a play execute sequentially per host. The `clients` map is locked during connection creation to prevent duplicate connections to the same host.