10. Go Beyond Local¶
Everything so far ran on one machine. This lesson leaves it. The remarkable part is how little changes: the topology, the group names, and the lifecycle verbs are all identical. What changes is the environment file — how Ansible reaches the targets and where files land on them.
Note
Estimated time: 30 minutes reading. Actually running the Kubernetes inventory needs a cluster; the distributed one needs machines. Builds on 9. Add the Extras.
What You Will Learn¶
- Why the same topology deploys to a laptop, a cluster, or sixteen machines with no structural change.
- The four inventory families and what each environment file sets.
- The two completely different things the word "namespace" means in a Kubernetes Fabric-X deployment.
- What you must do before a distributed inventory will run at all.
The Environment File Is the Only Difference¶
Put the four environment files side by side and the design is unmistakable.
| Family | ansible_connection |
ansible_host |
remote_deploy_dir |
|---|---|---|---|
| Local | local |
LOCAL_ANSIBLE_HOST or localhost |
{{ out_dir }}/local-deployment |
| Kubernetes | local |
<host>.<k8s_namespace>.svc.cluster.local |
{{ out_dir }}/k8s-deployment |
| OpenShift | local |
<host>.<k8s_namespace>.svc.cluster.local |
{{ out_dir }}/openshift-deployment |
| Distributed | ssh |
a real machine hostname | /root/perf-deployment |
Notice that Kubernetes and OpenShift both use ansible_connection: local. Ansible is not connecting into the cluster — it runs kubectl from your control node against the cluster API. The inventory hosts represent Kubernetes resources, and ansible_host is the in-cluster DNS name that other workloads use to reach the service.
There is a second address variable that carries the difference, and it is the one that trips people up:
| Variable | Means |
|---|---|
ansible_host |
How other services in the deployment reach this service |
actual_host |
How you, from outside, reach this service |
Locally they are the same. On Kubernetes, ansible_host is a cluster-internal DNS name while actual_host is the node address a NodePort is exposed on. Over SSH they collapse back together, because the machine hostname is reachable from both sides.
Switching Inventory Families¶
The mechanism is the same one you used in lesson 8:
Always make targets after switching, so the per-host Makefile targets describe the inventory you actually loaded. And tear down the previous deployment first — otherwise you leave orphaned containers behind that the new inventory knows nothing about.
Tip
Put ANSIBLE_INVENTORY in a .env file at the repository root. The Makefile includes it automatically, which removes an entire class of "why did that command do nothing" confusion.
Kubernetes¶
The Kubernetes family deploys the same logical Fabric-X services as Kubernetes workloads and services.
What you need:
- A reachable cluster and a working
kubectlcontext - The collection dependencies from
make install-deps, which includekubernetes.core
The extra variables the environment file adds:
| Variable | Default | Purpose |
|---|---|---|
k8s_namespace |
default |
The Kubernetes namespace everything is created in |
k8s_storage_size |
500Mi |
PersistentVolumeClaim size for stateful components |
actual_host |
K8S_NODE_IP or localhost |
Where NodePort services are reachable from outside |
For a remote cluster, tell the inventory where the nodes are:
Without it, actual_host falls back to localhost, which is correct for a local cluster (kind, minikube, Docker Desktop) and wrong for anything else. The symptom is a deployment that comes up cleanly and is unreachable — make ping fails against NodePort services while the pods are Running.
The lifecycle is unchanged:
Each role's Kubernetes task path ensures the Kubernetes namespace exists before it creates anything, so you do not have to create it yourself. Selected services are exposed with NodePort so they are usable from outside the cluster — the Block Explorer among them.
Tip
Debugging a Kubernetes deployment splits cleanly in two. Is the workload healthy? That is kubectl get pods -n <namespace> and kubectl logs. Is the configuration right? That is out/k8s-deployment/<host>/config/ on your control node, rendered from the inventory exactly as in lesson 7. The generated config is on your machine even though the process runs in the cluster.
Two Kinds of Namespace¶
This deserves its own section, because the word is badly overloaded and the two meanings have nothing to do with each other.
| "Namespace" | What it is | Set by | Created by |
|---|---|---|---|
| Kubernetes namespace | A Kubernetes API grouping for pods, services, and PVCs | k8s_namespace in the environment file |
each role's k8s task path, via the shared k8s role |
| Fabric-X namespace | The unit of ledger state isolation, with an endorsement policy | organization.namespaces on a host |
fxconfig, during make init |
flowchart TD
subgraph K8S["Kubernetes namespace: k8s_namespace"]
direction LR
POD1[orderer-router-1 pod]
POD2[committer-sidecar pod]
POD3[committer-validator pod]
end
K8S -.->|the ledger inside it contains| FXNS["Fabric-X namespaces: 'basic', '0', ...<br/>created by make init"]
One is infrastructure, the other is ledger semantics. A Kubernetes deployment has both, and the fact that make init "creates namespaces" refers only to the second kind. If make init fails, look at fxconfig and the committer endpoints — not at kubectl.
OpenShift¶
The OpenShift family is Kubernetes with a different exposure model: HTTP and HTTP2-capable ports are published through OpenShift Routes instead of NodePort or LoadBalancer services.
The environment file will try to discover the ingress domain itself with kubectl get ingresses.config/cluster, falling back to apps-crc.testing for a local CodeReady Containers cluster, so the export is only needed when discovery does not apply.
There is one local-cluster wrinkle worth knowing before you hit it. If OpenShift routes resolve to 127.0.0.1, binary CLIs on your machine work fine but containerized clients fail, because 127.0.0.1 inside a container is the container itself. The collection ships a playbook that writes the route hostnames into /etc/hosts pointing at your control node's real address:
Run it before starting Fabric-X. It is the same class of problem as the macOS host.docker.internal setup from lesson 2 — a container's idea of localhost is not yours.
Distributed over SSH¶
The distributed family is where the collection earns its keep: a performance-oriented topology spread over sixteen machines.
It is deliberately larger than the local samples:
- No Fabric CA — crypto is generated centrally with
cryptogenfor repeatability - 4 orderer groups with 2 batchers each
- 7 validators and 7 verifiers, 1 coordinator, 1 sidecar, 1 query service
- 3 YugabyteDB masters and 7 tablets
- 2 load generators
- 16 node exporters and 16 cAdvisors
Warning
This inventory does not run as shipped. distributed/group_vars/all/env.yaml contains sixteen placeholder machine names, host_machine_1 through host_machine_16, all pointing at linuxNamd64.cloud.com. Replace every one of them before doing anything else.
The checklist before your first run:
- Replace the placeholders. Set
host_machine_1…host_machine_16to real hostnames. - Confirm SSH access. The environment file uses
ansible_connection: sshwithansible_user: root. Key-based access must already work. - Check
ansible_python_interpreter. It is pinned to/usr/bin/python3; adjust it if your machines differ. - Point
remote_deploy_dirat fast storage. It defaults to/root/perf-deployment, and the comment in the file says to point it at your SSD mount. For a performance run this matters more than almost anything else in the inventory. - Install the prerequisites remotely. One command from your control node:
This runs install_prerequisites, which installs the container engine, tmux, OpenSSL, Git, Go, rsync, and chrony — and it picks one representative host per physical machine, so packages are not installed once per logical service.
- Review every port. Sixteen machines still means several logical services per machine, so the same uniqueness rules from lesson 5 apply.
Warning
make install-remote-node-deps needs sudo. Use a passwordless sudo user, or the playbook cannot complete.
There is a bonus target that is genuinely useful here and pointless locally:
make benchmark-volume # verify the volumes meet the sequential-write threshold
make run-command COMMAND="df -h" # one command across every machine
benchmark-volume checks the deployment volumes against a 1 GB/s sequential write threshold. If you are chasing throughput and the committer is slower than expected, storage is a good first suspect, and this tells you before you spend a day on it.
Choosing a Family¶
| Family | Use it when |
|---|---|
| Local | Learning, development, functional testing, reproducing a bug |
| Kubernetes | Validating manifests, service exposure, storage, cluster behaviour |
| OpenShift | The same, on an OpenShift cluster with Route-based exposure |
| Distributed | Performance evaluation and high-throughput work on real hardware |
The advice from lesson 8 still holds, and it holds harder here: change one dimension at a time. Do not debug a new topology and a new environment simultaneously. Get the topology working locally, then move it.
Exercise¶
Tip
This one is mostly reading — no cluster required.
- Switch
ANSIBLE_INVENTORYtoexamples/inventory/k8s/fabric-x.yamland useansible-inventoryto compare it against the local inventory. Which hosts changed, and which did not? - For the host
committer-sidecar, find bothansible_hostandactual_hostin the Kubernetes inventory and explain what each is for. - Then, without running anything: you have a remote Kubernetes cluster, you deploy successfully, every pod is
Running, andmake pingfails on every NodePort service. What is the single most likely cause?
Solution
Part 1:
export ANSIBLE_INVENTORY=examples/inventory/k8s/fabric-x.yaml
.venv/bin/ansible-inventory --graph
# and compare against the local one
ANSIBLE_INVENTORY=examples/inventory/local/fabric-x.yaml \
.venv/bin/ansible-inventory --graph
The answer is the point of the lesson: the host list and the group tree are essentially the same. The same five Fabric CA servers, the same four orderer groups, the same committer services, the same Block Explorer, the same monitoring components. What changed is not the topology but the environment: the connection model, the addresses, the deploy directory, and the added Kubernetes settings.
Part 2:
Remember from lesson 5 that this prints the definitions, not the values, so you will see the templates from the environment file:
"ansible_host": "{{ inventory_hostname }}.{{ k8s_namespace | default('default') }}.svc.cluster.local",
"actual_host": "{{ lookup('env', 'K8S_NODE_IP') or 'localhost' }}",
ansible_hostbecomescommitter-sidecar.default.svc.cluster.local— the in-cluster DNS name. This is what the other services put in their configuration to reach the sidecar: the coordinator, the Block Explorer, the EVM gateway.actual_hostbecomesK8S_NODE_IPif you exported it, otherwiselocalhost— the address you use from outside the cluster, through a NodePort.
Both are needed, because a service has two addresses in a Kubernetes deployment and the generated configuration has to use the right one in each place.
Part 3: K8S_NODE_IP is not set. actual_host fell back to localhost, so every externally-facing endpoint was rendered as localhost:<nodePort> and every external check is testing your own machine rather than the cluster node.
Note that fixing the variable is not enough on its own — the wrong value is baked into the rendered configuration, so configs has to re-render before a restart will help. That is the same lesson as the exercise in lesson 6, and it is the single most common way to lose an hour with this collection.
Next¶
| Previous | Next |
|---|---|
| 9. Add the Extras | 11. Write Your Own Inventory |