Note 01
Terraform to Flux through one ConfigMap
The GitOps Bridge pattern for Flux: Terraform publishes the AWS facts once, every manifest substitutes them, and no manifest names an account, a region or an ARN.
- Terraform
- Flux
- GitOps
- AWS
The gap between the two tools
Add-ons on EKS need facts only the infrastructure code knows: the Karpenter interruption queue, the role ARN for the load balancer controller, the VPC ID, the KMS key for node volumes. Terraform creates those things; Flux installs the charts that need them. Copying the values into Git by hand drifts on the next apply. Letting Terraform install the charts with the Helm provider makes Terraform own state that Flux then changes under it.
The GitOps Bridge project names the pattern that closes this gap: the infrastructure tool creates the cloud resources and stores their metadata where the GitOps engine can read it; the GitOps engine installs everything and passes the metadata to the charts. Its reference implementation is for Argo CD: Terraform writes the cluster secret, add-on switches become labels, values become annotations, and ApplicationSets select clusters by label and template values from the annotations. The Flux side of the project is still in progress, so this is how the platform does it for Flux.
One ConfigMap, written once
Terraform writes a single ConfigMap, cell-metadata, into flux-system. It holds identifiers only, never a secret value:
resource "kubernetes_config_map_v1" "cell_metadata" {
metadata {
name = "cell-metadata"
namespace = "flux-system"
}
data = {
cell_name = var.name
aws_region = local.region
cluster_name = module.eks.cluster_name
karpenter_interruption_queue = module.karpenter.queue_name
karpenter_node_role = module.karpenter.node_iam_role_name
app_role_arn = aws_iam_role.app_abac.arn
admission_action = var.environment == "dev" ? "Audit" : "Deny"
}
}Every Flux Kustomization substitutes from it after building, so a manifest simply writes ${cluster_name}:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: controllers
namespace: flux-system
spec:
path: ./infrastructure/controllers
postBuild:
substituteFrom:
- { kind: ConfigMap, name: cell-metadata }What differs from the Argo CD bridge
- Which add-ons a cell runs is decided in Git, not by labels Terraform writes. Each cell's directory lists its layers and optional components. A reviewer sees the full composition in one pull request; Terraform never decides what is installed.
- Strict substitution. Flux fails a reconciliation when a manifest asks for a variable the ConfigMap does not have, so a missing value is a loud error, not an empty string in a chart.
- Terraform installs only what must exist before Flux: the CNI, the egress gateway when the cell has one, and Flux itself. Everything else is Flux's.
- Identity before workload. Terraform creates a workload's role and Pod Identity association before Flux deploys the workload, so a pod never starts without its permissions and Flux never needs IAM rights.
Rules that keep it honest
- Identifiers only. A database endpoint is fine; its password is not. Secrets reach the cluster through External Secrets or Pod Identity.
- The ConfigMap carries a schema number. Adding or renaming a key is a change on both sides, reviewed together.
- CI renders every layer with a sample ConfigMap in strict mode, so a typo in
${variable}fails the pull request, not the cluster.
The result is one GitOps tree for every cell. A new region is a new Terraform stack and a new directory that lists what the cell runs; no manifest is copied.
Start a conversation
Bring your infrastructure
into focus.
Tell me what you are building, operating, or trying to improve.