Architecture¶
The controller turns a Terraform object into Terraform or OpenTofu runs
executed in a short-lived runner Pod, and reports the result back through
the object's status so that Argo CD can display it like any other resource.
flowchart LR
subgraph argocd[Argo CD namespace]
app[Application]
srv[argocd-server<br/>UI extension]
cm[argocd-cm<br/>health + actions Lua]
end
subgraph sys[argocd-terraform-controller-system]
mgr[Controller manager]
end
subgraph ns[Terraform object's namespace]
tf[Terraform]
runner[Runner Pod<br/>name-tf-runner]
state[(tfstate Secret<br/>+ lock Lease)]
plan[(tfplan Secret<br/>+ readable ConfigMap)]
outputs[(outputs Secret)]
tls[(runner TLS Secret)]
end
src[(Git / OCI source)]
cloud[(Providers:<br/>AWS, GCP, Kubernetes, …)]
app -- sync --> tf
srv -. Argo CD API .-> app
cm -. health, actions .-> tf
mgr -- watch / status --> tf
mgr -- fetch --> src
mgr -- create --> runner
mgr -- gRPC over mTLS :30000 --> runner
mgr --> tls
runner --> state
runner --> plan
runner --> outputs
runner --> cloud
Components¶
| Component | Runs where | Role |
|---|---|---|
Controller manager (cmd/manager) |
Deployment in argocd-terraform-controller-system, one leader |
Watches Terraform objects; resolves and fetches sources; creates runner Pods; drives them over gRPC; writes status, conditions and events; owns the mTLS CA |
Runner (cmd/runner) |
One Pod per Terraform object, in the object's namespace, only for the duration of a run | Receives the source archive, generates backend/variables files, executes terraform/tofu through terraform-exec, stores plans and outputs as Secrets/ConfigMaps using its own ServiceAccount (tf-runner) |
Argo CD customizations (config/argocd) |
argocd-cm and an init container on argocd-server |
Health assessment, resource actions (approve, re-plan, suspend, resume, force-unlock) and the UI extension; see Argo CD integration |
Only the controller has credentials to read sources; only the runner has permissions in the object's namespace. The two meet over a per-namespace mutual-TLS gRPC connection.
A reconciliation, step by step¶
- Trigger. The object was created or changed (a new
metadata.generation),spec.intervalelapsed, theinfra.argocd.io/reconcileannotation was set, or a previous run asked for a retry. Status-only updates do not trigger runs. - Dependencies. If
spec.dependsOnis set, every dependency must be Ready at its current generation (and, for the same source, at the same revision); otherwise the object isProgressing/DependencyNotReadyand is retried. A finalizer is added to each dependency so it cannot be deleted first. - Source. The controller resolves the revision (
git ls-remote, or the OCI manifest digest). If it changed since the last run, the tree is fetched, filtered byignore, and archived. Argo CD repository credentials can be reused (Sources). - Runner. A Pod
<name>-tf-runneris created in the object's namespace fromspec.runner(image, ServiceAccount,podTemplate) with a mounted server certificate issued by the controller's CA. The controller waits for it to become ready (--runner-creation-timeout) and dials it by Pod IP. - Setup. The archive is uploaded; the runner writes
backend_override.tf(the Kubernetes backend by default, see State),generated.auto.tfvars.jsonfromspec.vars/varsFrom/values, anyspec.fileMappings, and the.tfrcfromcliConfigSecretRef; then runsinit(with-upgradeand-force-copy) and selects the workspace. - Drift detection. When nothing changed since the last apply, a
refresh-only plan checks the real infrastructure against the state. No
drift → done (
Ready/NoDrift). Drift →DriftDetected; inautomode the run continues to re-plan and apply, otherwise it stops and reports. See Drift detection. - Plan.
plan -outproduces a binary plan stored in Secrettfplan-<workspace>-<name>and a human-readable copy in ConfigMaptfplan-<workspace>-<name>-readable. The plan ID (plan-<ref>-<sha>) goes tostatus.plan.pending. Without approval the run stops here; Argo CD shows Suspended. See Plan approval. - Apply. With
approvePlan: auto,spec.force, or a matching plan ID the controller re-plans once more (to make sure the approved plan is still current) and applies. Failures setReady=Falsewith aTFExec*reason and schedule a retry. - Outputs, inventory, health checks. Outputs are written to
spec.writeOutputsToSecret, the list of managed resources is recorded instatus.inventory, andspec.healthChecksrun against the outputs. See Outputs and Dependencies and health checks. - Cleanup. The working directory is removed, the runner Pod is deleted
(
spec.runner.alwaysCleanup, default) and the object is requeued afterspec.interval.
Deletion follows the same path through a finalizer: with
spec.destroyResourcesOnDeletion a destroy plan is applied first; plan and
output objects are removed; the state Secret is kept unless
spec.tfstate.deleteOnDestroy is set. See
Destroy and deletion.
Status model¶
The object carries standard metav1.Conditions. Ready is the one Argo CD's
health check reads:
Ready |
Reason (examples) | Meaning | Argo CD health |
|---|---|---|---|
True |
TerraformAppliedSucceed, NoDrift, TerraformOutputsWritten, HealthChecksSucceeded, TerraformPlannedNoChanges |
Last run succeeded and the infrastructure matches | Healthy |
Unknown |
Progressing, TerraformPlannedWithChanges, DependencyNotReady |
Working, or waiting for a human | Progressing, or Suspended when a plan awaits approval |
False |
TFExecPlanFailed, TFExecApplyFailed, SourceFetchFailed, LockHeld, DriftDetected, HealthChecksFailed, RetryLimitReached, AccessDenied |
Something needs attention | Degraded |
| any | Suspended |
spec.suspend: true |
Suspended |
Additional conditions (Plan, Apply, Output, HealthCheck, StateLocked,
Reconciling, Stalled) record the last result of each phase. Events with the
same reasons are emitted on the object; the Argo CD UI shows them under the
Terraform tab and in the dashboard. The full list is in the
specification reference.
Isolation model¶
- Credentials split. Source credentials (Git/OCI, including Argo CD's) are
read by the controller and never reach the runner; the runner receives a
tarball. Provider credentials given through
env/envFromor workload identity reach the runner only;fileMappingscontent is read by the controller and forwarded over the mTLS channel (Variables and credentials). The controller itself never holds a cloud identity or runs the binary. - Per-namespace runners. A runner runs as the
tf-runnerServiceAccount of the object's namespace with a Role limited to Secrets, ConfigMaps and Leases there. Different teams' namespaces get different identities, and cloud permissions can be bound per namespace (or per object withspec.runner.serviceAccountName). - mTLS. The controller runs a CA (rotated,
--ca-validity-duration) and issues a server certificate per namespace (terraform-runner.tlsSecret). The runner only accepts the controller's client certificate; nothing else can instruct it. - Hardened Pods. Manager and runner run as non-root with a read-only root
filesystem, all capabilities dropped and a
RuntimeDefaultseccomp profile. The runner's/tmpand/home/runnerareemptyDirs that die with the Pod. - No Argo CD privileges. The UI extension uses the logged-in user's Argo CD session and RBAC; the controller only needs read access to Argo CD's repository Secrets, and only if that feature is enabled.
See Security for the operator's checklist.
Repository map¶
For contributors, the code follows the component split above:
| Path | Contents |
|---|---|
api/v1alpha1 |
Terraform types, condition helpers, naming helpers (StateSecretName, RunnerPodName) |
internal/controller |
Reconciler: setup, drift, plan, apply, outputs, finalizer, runner Pod management |
internal/runner |
gRPC server executed inside the runner image (runner.proto) |
internal/mtls |
CA and certificate rotation |
internal/source |
Git and OCI fetchers, Argo CD credential lookup |
internal/plan, internal/tarball |
Plan Secret chunking, source archives |
config/ |
Kustomize: CRD, RBAC, manager, runner identity, Argo CD component, samples |
charts/ |
Helm chart |
ui/extension |
Argo CD UI extension (TypeScript/React) |
More in the Developer Guide.