Managing the whole infrastructure declaratively: integrating Terraform and Flux through the official provider, bootstrapping clusters via Terraform, understanding the infrastructure layers, and GitOps for infrastructure with Crossplane, the Terraform Controller, and Atlantis toward full-stack GitOps.

In episode 28 you tested everything inside the cluster: manifests are validated, changes are tested with dry-run, preview environments work, and chaos engineering proved resilience. But there's one irony that can't be hidden: how is the cluster itself created? As long as infrastructure is still created with clicks in the cloud console or manual commands, GitOps is only half done.
This is a real problem. Applications inside the cluster are declarative and reproducible from Git, but the VPC, cluster, and platform underneath might still be haphazard. If we're honest about the GitOps principle — desired state stored in Git, and the system moving toward that state — then infrastructure must enter the same mechanism. This is where Terraform and its integration with Flux come in.
In this episode we'll close that gap: Terraform together with Flux with the official provider and bootstrap via Terraform, infrastructure layers from network to applications, GitOps for infrastructure with Crossplane, the Terraform Controller, and Atlantis, and the full-stack GitOps vision where the entire stack lives in Git.
Terraform is the most established IaC tool: it declares the desired infrastructure state and performs plan and apply to reach that state. Common Terraform scope in the Flux world:
With Terraform, a cluster can be destroyed and rebuilt consistently — a perfect foundation for the disaster recovery in episode 26.
Flux has an official Terraform provider that allows installing and bootstrapping Flux directly from Terraform. This provider needs Kubernetes access configuration and Git configuration for the bootstrap:
provider "flux" {
kubernetes = {
host = module.eks.cluster_endpoint
cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
token = data.aws_eks_cluster_auth.this.token
}
git = {
url = "https://github.com/devvnull/gitops-production.git"
branch = "main"
http = {
username = "gitops-bot"
password = var.github_token
}
}
}The Flux provider reads the cluster connection configuration from the existing Kubernetes provider, so there's no need for duplicated credentials.
With the Flux provider, the bootstrap that was previously done through the CLI can be declared as a Terraform resource:
resource "flux_bootstrap_git" "this" {
path = "clusters/production"
version = "v2.3.0"
components_extra = ["image-reflector-controller", "image-automation-controller"]
}When terraform apply runs, Terraform installs the Flux controllers into the cluster and writes the bootstrap Kustomization to the Git repository. GitHub credentials are sent through variables, not stored in code. The result: terraform apply produces a cluster with Flux installed and a GitOps repository containing the bootstrap configuration.
Important
Because Terraform stores state (including credentials in some cases) in the tfstate file, store the state in a remote backend — for example S3 with an encrypted bucket — not locally. Terraform state is an asset that must be guarded as carefully as the configuration itself.
Terraform state records the managed resources — the operational source of truth for what has been created. Healthy practices:
Well-managed state makes terraform plan accurate and terraform destroy safe.
Good infrastructure is built in layers, and every layer has its own reason to be managed declaratively. The most basic layer is the network: VPC, subnets, route tables, security groups, and gateways. This layer rarely changes and has a big impact if wrong — exactly the reason it's best suited for strict IaC.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
}
resource "aws_subnet" "private_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-southeast-1a"
}The next layer is the cluster itself. Terraform provisions clusters using each cloud provider — eks for AWS, gke for Google Cloud, azurerm for Azure — or a generic provider like hcloud for k3s. The output of this layer (endpoints, credentials) becomes the input for the platform layer.
module "eks" {
source = "terraform-aws-modules/eks/aws"
cluster_name = "production"
cluster_version = "1.30"
vpc_id = aws_vpc.main.id
subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
enable_irsa = true
}The platform layer contains what runs on top of the cluster: Flux itself, the ingress operator, cert-manager, monitoring, and other shared components. Part is managed by Terraform (for example Flux through the provider), part by Flux through Kustomizations in the GitOps repository. This is where the two streams meet: Terraform makes sure the core components are installed, Flux keeps their configuration in sync with Git.
The topmost layer is applications — entirely the GitOps domain. Application manifests, HelmReleases, and application Kustomizations live in the GitOps repository and are managed by Flux without Terraform involvement. This division is important: Terraform handles what rarely changes, Flux handles what constantly changes. Each uses the tool most suitable for its lifecycle.
| Layer | Tool | Change frequency |
|---|---|---|
| Network | Terraform | Very rare |
| Cluster | Terraform | Rare |
| Platform | Terraform + Flux | Sometimes |
| Application | Flux only | Often |
After applications enter Git, the next question arises: can infrastructure also enter Git and be reconciled by a controller like Flux? Crossplane answers this by turning cloud infrastructure into Kubernetes resources. Crossplane runs inside the cluster, and with providers like provider-aws, it can create a VPC or database directly from a manifest:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
metadata:
name: checkout-db
namespace: apps
spec:
forProvider:
dbInstanceClass: db.t3.medium
engine: postgres
allocatedStorage: 20
providerConfigRef:
name: defaultBecause Crossplane resources are ordinary Kubernetes objects, Flux can manage them like any other resource — infrastructure now truly lives in Git and participates in pull requests and review.
Tip
Crossplane and Flux complement each other: Flux keeps Crossplane (and its policies) installed, while Crossplane creates the cloud resources declared in the GitOps repository. The combination makes the whole infrastructure a single GitOps workflow.
Another alternative for managing Terraform through GitOps is the Terraform Controller (tf-controller). It runs Terraform inside the cluster in response to manifests, similar to how Flux runs Kustomize. An example:
apiVersion: infra.contrib.fluxcd.io/v1alpha2
kind: Terraform
metadata:
name: checkout-db
namespace: apps
spec:
path: ./terraform/checkout-db
interval: 1h
sourceRef:
kind: GitRepository
name: apps
approvePlan: auto
writeOutputsToSecret:
name: checkout-db-outputsWith tf-controller, Terraform changes also flow through Git and PRs, running plan automatically and apply after approval. The Terraform lifecycle (drift detection, destroy) is run continuously by the controller, not only on manual terraform apply.
Atlantis takes a different approach: it automates a pull-request-based Terraform review flow. When a PR changes Terraform code, Atlantis runs terraform plan and posts the result as a comment on the PR. After approval, the /apply comment triggers terraform apply. Atlantis maintains two things at once: all changes go through review and the Terraform state always stays in sync with the PR.
A common pattern combines all three:
Full-stack GitOps means no layer is left outside Git. Starting from infrastructure: VPC, cluster, and cloud services are managed from the Git repository (through Terraform under Atlantis or Crossplane), via pull requests with automatic plans.
The platform layer — Flux, operators, ingress, monitoring — is declared in the GitOps repository as Kustomizations or HelmReleases. The platform team changes the shared stack through PRs, and Flux applies them with the same reconciliation mechanism as applications.
The application layer has been in Git from the start: manifests, images, and application configuration are deployed by Flux. The CI pipeline (episode 27) only writes images and manifests; everything flows through Git.
When all layers live in Git, the whole organization uses one workflow:
Tip
Start full-stack GitOps from the applications, then expand to the platform, and finally to infrastructure — in order of difficulty. Every layer that enters Git reduces the knowledge that only lives in one person's head and increases confidence for fast changes.
In this episode 29 you unified the entire stack in one mechanism: Terraform and Flux through the official provider and bootstrap via Terraform, healthy state management with a remote backend, infrastructure layers from network to applications with the right tool split, GitOps for infrastructure through Crossplane, the Terraform Controller, and Atlantis, and the full-stack GitOps vision with a unified workflow from pull request to automatic execution.
The key takeaways:
terraform apply, not manual steps.With this, almost your entire GitOps journey is complete. In the next episode, episode 30, we'll discuss Cloud Provider Patterns — applying all the concepts above to real cloud providers: bootstrapping Flux on EKS, GKE, and AKS, multi-region patterns, and integration with managed services and cloud-native authentication. Keep up the momentum!