ADR 005 — Infrastructure as Code: OpenTofu + Helm + Ansible (+ Docker Compose, Air-gap)
Status: Accepted Date: 2026-04-23 Deciders: Jerome Related: prd-detection-tiering, detection-multitier-architecture, brainstorm-deployment-models, saas-security-legal-framework
Context
Noxys sells into three very different customer topologies:
- Cloud SaaS (Noxys-managed) — we own Kubernetes clusters on EU cloud providers
- Self-hosted K8s — regulated customer with their own cluster (OpenShift / Rancher / vanilla EKS/GKE/AKS)
- Self-hosted VM / bare-metal — smaller customer, one or two Linux VMs, no Kubernetes
- Air-gap — government / defence, no internet access, offline install
No single IaC tool covers all four. We must pick a stack that is sovereign-compatible (part of our positioning) and gives us first-class ergonomics in each topology without forcing customers into our tooling choices.
Decision
Three first-class IaC tools, plus Docker Compose and an offline bundle.
| Topology | Primary tool |
|---|---|
| Cloud SaaS (Noxys clusters) | OpenTofu provisions cloud resources → Helm deploys workloads |
| Self-hosted K8s (customer cluster) | Helm chart |
| Self-hosted VM / bare-metal | Ansible role |
| Self-hosted single-node POC / dev | Docker Compose |
| Air-gap | Tar bundle with cosign-signed image + pre-downloaded models + install script |
Single source of truth for application configuration lives in the Helm values schema. OpenTofu and Ansible consume the same config surface where relevant; air-gap bundle embeds a pre-rendered Compose file or Helm template.
Consequences
Positive
- Customer choice: we meet them where they already are (K8s shop → Helm, Ansible shop → Ansible, no-ops shop → Compose)
- Sovereign-first: OpenTofu (not Terraform) aligns with our open-source EU positioning
- Air-gap story is a natural extension of the signed image pipeline
- Helm values remain the config contract; reduces drift across deployment modes
Negative
- Three IaC skill sets in the team. Mitigated by: only ops team touches OpenTofu; Ansible role is thin (it wraps Docker, not bespoke config management); Helm values documented thoroughly.
- CI must smoke-test all four modes (Kind for K8s, Vagrant/Docker-in-Docker for Ansible role, dry-run for OpenTofu, compose up for Compose).
- Air-gap bundle rebuild is a separate release artifact — adds ~10 min to the release pipeline.
Alternatives Considered
Alternative A — Terraform instead of OpenTofu
- Pros: larger ecosystem, most customer ops teams know it
- Cons: Terraform moved to BSL (Business Source License) in Aug 2023. Using BSL in our own infra while selling a "sovereign open" pitch is a narrative contradiction customers in sovereign sales notice. OpenTofu (Linux Foundation stewardship, MPL-2.0) resolves this.
- Note: OpenTofu is drop-in compatible with Terraform providers; our customers can still use Terraform on their side.
- Rejected: license alignment with brand positioning is load-bearing
Alternative B — Pulumi (TypeScript / Python / Go)
- Pros: real language, test-friendly, good abstraction
- Cons: team is not Pulumi-native; state management (SaaS backend or self-hosted) adds complexity; language lock-in per project; smaller community in EU sovereign ops; customer ops teams rarely run it
- Rejected: learning curve + ecosystem mismatch outweighs the ergonomic gains at our stage
Alternative C — Pure Kubernetes manifests (no Helm)
- Pros: minimal tooling, gitops-friendly (Kustomize + ArgoCD)
- Cons: config variance per tier / per customer exploded quickly in our prototypes; Helm's value templating + lint + chart repo is exactly what we need; Kustomize overlays become unreadable at > 3 variants
- Rejected: Helm is the K8s packaging lingua franca; we can layer Kustomize later if needed
Alternative D — Ansible only (no K8s, no OpenTofu)
- Pros: one tool, everywhere
- Cons: Ansible at K8s scale is painful; cloud SaaS economics demand K8s-native autoscaling; OpenTofu much better for cloud-resource lifecycle
- Rejected: wrong tool for cloud SaaS tier
Alternative E — Crossplane or other K8s-native IaC
- Pros: one control plane
- Cons: bleeding-edge, non-standard customer skill, doesn't solve the bare-metal or air-gap problems
- Rejected: mismatch with customer reality
Enforcement
- Helm chart is the canonical manifest; OpenTofu module composes it (helm_release)
- Ansible role's job is only to install Docker + pull container + systemd unit + TLS; it does not re-implement app config
- Docker Compose file is generated from a reduced Helm template (chart helper)
- CI matrix:
kind,vagrant(or skip if too heavy),tofu plan -target,docker-compose up - Air-gap bundle generation is a Makefile target; release pipeline publishes the tarball alongside the Helm chart
Follow-up Actions
- DT-5 delivers all five modes
- Document trade-offs per mode in
docs/ops/sidecar-deployment.md - First-customer pilot (Business tier self-host) validates Helm path
- First air-gap pilot validates offline bundle
References
- prd-detection-tiering
- detection-multitier-architecture
- brainstorm-deployment-models
- saas-security-legal-framework
- OpenTofu: https://opentofu.org
- Helm: https://helm.sh
- Ansible: https://www.ansible.com