The Evolution of Declarative Infrastructure
For nearly a decade, Infrastructure as Code (IaC) has been dominated by static, client-side execution tools. HashiCorp Terraform established the industry standard by introducing a declarative syntax (HCL) to define the desired state of cloud resources, mapping them to real-world APIs via a state file. However, as enterprise environments scale across multiple public clouds—including AWS, Azure, GCP, and Oracle Cloud Infrastructure (OCI)—the limitations of static IaC become apparent.
Traditional IaC operates on a "push" model: an engineer writes code, executes a plan, and applies the changes. Once the execution pipeline finishes, the tool stops running. If an operator manually alters a security group in the AWS Console or deletes a firewall rule in GCP three hours later, the system enters a state of configuration drift. This drift remains undetected until the next scheduled pipeline run, introducing severe security vulnerabilities and unpredictable financial liabilities.
To solve this, a new paradigm has emerged: Control Plane-driven Infrastructure. By leveraging the Kubernetes API and controller pattern, Crossplane transforms infrastructure management from a periodic batch job into an active, continuously reconciling loop. Instead of merely declaring state at a point in time, Crossplane constantly monitors the live cloud environment, actively correcting drift back to the declared state. For organizations leveraging SRE and infrastructure operations solutions, understanding how to orchestrate these two technologies is critical to achieving high availability, robust security, and cost control.
Architectural Mechanics: Terraform vs. Crossplane
To design an effective cross-cloud infrastructure strategy, we must first analyze the underlying execution engines of both Terraform and Crossplane. They are not mutually exclusive; rather, they operate on fundamentally different execution loops.
Terraform: The Directed Acyclic Graph (DAG) and State File
Terraform relies on a client-side execution engine that reads configuration files and constructs a Directed Acyclic Graph (DAG) of resource dependencies. The lifecycle of a Terraform deployment follows a strict sequence:
Initialization (
terraform init): Downloads the necessary provider binaries.Planning (
terraform plan): Queries the current state of resources via cloud APIs, compares them to the local.tfstatefile, and calculates the delta required to reach the target configuration.Application (
terraform apply): Executes API calls sequentially or concurrently based on the DAG, updating the state file upon success.
The state file is the single source of truth for Terraform. If this file is corrupted, lost, or falls out of sync, the infrastructure becomes unmanageable. Furthermore, concurrency is managed via state locking (typically using AWS DynamoDB or Consul), which can block deployment pipelines in large, fast-moving teams.
Crossplane: The Kubernetes Reconciliation Loop
Crossplane runs natively inside a Kubernetes cluster, turning the cluster into a universal control plane. It extends the Kubernetes API using Custom Resource Definitions (CRDs) to represent external cloud services as native Kubernetes objects. Crossplane's architecture consists of three core layers:
Managed Resources (MRs): Individual cloud resources (e.g., an AWS RDS instance, an Azure SQL database) represented as Kubernetes CRDs.
Composite Resource Definitions (XRDs): Custom APIs defined by infrastructure teams that bundle multiple Managed Resources into a single, reusable blueprint (e.g., a "SecureDatabase" XRD that provisions an RDS instance, its subnet groups, IAM roles, and KMS keys).
Compositions: The actual implementation templates that map XRDs to specific cloud providers.
The Crossplane controller runs an infinite reconciliation loop (typically every 60 seconds). It continuously observes the actual state of the cloud resources, compares it to the desired state declared in the Kubernetes etcd database, and applies the necessary mutations to eliminate drift without human intervention.
+-----------------------------------------------------------------------------+
| KUBERNETES CONTROL PLANE |
| |
| +------------------+ Reconcile +----------------------------+ |
| | Custom Resource | <-----------------> | Crossplane Controller | |
| | (Desired State)| | (Observe -> Analyze -> | |
| +------------------+ | Act Loop) | |
+--------------------------------------------+------------+---------------+---+
|
| Reconcile State
v
+------------+---------------+
| Cloud Provider |
| (AWS, Azure, GCP, OCI) |
+----------------------------+
Designing a Hybrid Control Plane: "Better Together"
Enterprise platforms rarely migrate entirely to a single tool. The most resilient architectures utilize a hybrid model: using Terraform for bootstrapping foundational, slow-changing network infrastructure, and Crossplane for dynamic, application-level resource provisioning.
The Division of Responsibilities
In a production-grade multi-cloud topology, responsibilities should be partitioned based on resource lifecycles:
Infrastructure Layer | Recommended Tool | Rationale |
|---|---|---|
Foundational Network (VPCs, Subnets, Transit Gateways, Direct Connects) | Terraform | These resources change infrequently and require strict, manual change-management reviews. Terraform's plan/apply gates prevent accidental network disruption. |
Base Compute & Clusters (EKS, AKS, GKE, IAM OIDC providers) | Terraform | Bootstrapping the Kubernetes clusters that will host Crossplane must occur before Crossplane can execute. |
Application Dependencies (S3 Buckets, RDS Instances, SQ/MQ Queues, Cache Clusters) | Crossplane | Application teams can self-service these resources using Kubernetes manifests (YAML) directly within their GitOps pipelines (e.g., ArgoCD or Flux). |
Global Edge (Cloudflare, DNS, CDN Routing) | Hybrid | Terraform handles root domains; Crossplane manages dynamic subdomains and routing records bound to application deployments. |
Bootstrapping Crossplane with Terraform
To implement this hybrid architecture, we first write a Terraform configuration to provision an AWS EKS cluster, install Helm, and deploy Crossplane with the appropriate AWS Provider. This ensures that the foundational control plane itself is managed as code.
# bootstrap.tf
provider "aws" {
region = "us-west-2"
}
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 19.0"
cluster_name = "control-plane-cluster"
cluster_version = "1.28"
vpc_id = var.vpc_id
subnet_ids = var.private_subnet_ids
eks_managed_node_groups = {
default = {
min_size = 2
max_size = 5
desired_size = 3
instance_types = ["t3.medium"]
}
}
}
provider "helm" {
kubernetes {
host = module.eks.cluster_endpoint
cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
exec {
api_version = "client.authentication.k8s.io/v1beta1"
args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
command = "aws"
}
}
}
resource "helm_release" "crossplane" {
name = "crossplane"
repository = "https://charts.crossplane.io/stable"
chart = "crossplane"
namespace = "upbound-system"
create_namespace = true
set {
name = "args"
value = "{--enable-usages}"
}
}
Deep Dive: Building a Multi-Cloud Database Composition
Once Crossplane is bootstrapped, we can leverage its power to create a cloud-agnostic abstraction for application teams. Below, we define a Composite Resource Definition (XRD) for a PostgreSQL database, followed by an AWS Composition. This abstraction allows developers to request a database without needing to understand the underlying cloud provider's API or security configurations.
1. Defining the Custom API (XRD)
This XRD defines the schema for our custom CompositePostgresInstance resource. It mandates parameters like storage size and engine version while hiding complex networking and parameter group variables.
# postgres-xrd.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: compositepostgresinstances.database.cloudatler.com
spec:
group: database.cloudatler.com
names:
kind: CompositePostgresInstance
plural: compositepostgresinstances
claimNames:
kind: PostgresInstance
plural: postgresinstances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
storageGB:
type: integer
version:
type: string
enum: ["12", "13", "14", "15"]
required:
- storageGB
- version
required:
- parameters
2. Implementing the AWS Composition
The Composition defines how the CompositePostgresInstance is mapped to actual AWS resources: an RDS DBInstance and a SubnetGroup. It dynamically injects enterprise-mandated security defaults, such as enabling storage encryption and disabling public accessibility.
# aws-postgres-composition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: aws-postgres-production
labels:
provider: aws
environment: production
spec:
compositeDeletePolicy: Background
resources:
- name: rdsinstance
base:
apiVersion: database.aws.upbound.io/v1beta1
kind: DBInstance
spec:
forProvider:
region: us-west-2
dbInstanceClass: db.t4g.medium
engine: postgres
allocatedStorage: 20
skipFinalSnapshot: true
publiclyAccessible: false
storageEncrypted: true
writeConnectionSecretToRef:
namespace: crossplane-system
patches:
- fromFieldPath: "spec.parameters.storageGB"
toFieldPath: "spec.forProvider.allocatedStorage"
- fromFieldPath: "spec.parameters.version"
toFieldPath: "spec.forProvider.engineVersion"
- fromFieldPath: "metadata.name"
toFieldPath: "spec.writeConnectionSecretToRef.name"
transforms:
- type: string
string:
fmt: "%s-pg-secret"
associatedWith:
apiVersion: database.cloudatler.com/v1alpha1
kind: CompositePostgresInstance
Enterprise Security, OIDC, and Policy-as-Code
In a cross-cloud environment, security cannot rely on static API keys or long-lived credentials stored in Kubernetes secrets. Compromising these credentials exposes your entire multi-cloud footprint. Instead, enterprises must implement OpenID Connect (OIDC) federation between their Kubernetes control plane and the cloud providers.
Passwordless Authentication via OIDC
By establishing an OIDC trust relationship, the Crossplane AWS Provider assumes an IAM Role using AWS Security Token Service (STS), while the Azure Provider utilizes Managed Identities via Workload Identity. This eliminates secret rotation overhead and minimizes the attack surface.
For example, in AWS, the Trust Policy for the Crossplane IAM Role must restrict access to the specific Kubernetes ServiceAccount running the provider controller:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/EXAMPLEDOCUMENT"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-west-2.amazonaws.com/id/EXAMPLEDOCUMENT:sub": "system:serviceaccount:upbound-system:provider-aws-*"
}
}
}
]
}
Enforcing Policy-as-Code
While Terraform relies on static analysis tools like Trivy or Checkov during the CI/CD pipeline, Crossplane relies on active, runtime admission controllers. Using Open Policy Agent (OPA) Gatekeeper or Kyverno, security teams can intercept incoming PostgresInstance claims before they are processed by Crossplane. This ensures that any attempt to request unencrypted storage or publicly accessible databases is blocked at the API gateway level.
To implement comprehensive enterprise governance, security teams should pair these runtime checks with CloudAtler's Security Management platform, which monitors both static IaC configurations and active Crossplane controllers, providing a single pane of glass for compliance audits. By defining declarative security guardrails, organizations can automatically block the creation of non-compliant resources across all cloud environments.
FinOps and Resource Lifecycle Optimization
Multi-cloud architectures frequently suffer from "cloud sprawl"—orphaned volumes, unattached elastic IPs, and over-provisioned staging databases that silently inflate the monthly bill. This is where the structural differences between Terraform and Crossplane directly impact your bottom line.
Preventing Resource Leakage with Crossplane Usages
In traditional Kubernetes environments, deleting a Namespace often leaves behind the cloud resources managed by Crossplane if the deletion order is not strictly managed. To prevent these orphaned resources, Crossplane introduced the "Usages" API. Usages create explicit dependency graphs between resources, preventing a parent resource (like a Kubernetes Namespace or an application deployment) from being deleted until all of its bound cloud resources are safely decommissioned.
For instance, if an application deployment requires a Redis cache, a Usage record can be created to bind them. If the application is deleted, Crossplane automatically triggers the deletion of the Redis cache first, ensuring no orphaned billing elements remain.
Active Cost Optimization and Tagging
A core pillar of FinOps is accurate cost allocation. Without precise metadata, tracking down the owner of a $2,000/month database is nearly impossible. Both Terraform and Crossplane support resource tagging, but Crossplane's dynamic patching allows for real-time tag propagation based on the namespace or team submitting the claim.
By integrating these metadata schemas with automated tagging and resource allocation engines, teams can ensure that every single resource provisioned via the control plane carries the necessary cost-center, environment, and owner tags. These tags then feed into a unified cloud financial operations platform to generate real-time cost-impact calculations, allowing engineering leaders to see the exact financial footprint of their declarative manifests.
Step-by-Step Implementation: The Multi-Cloud Pipeline
To illustrate the operational flow of this unified declarative model, let us trace the lifecycle of an application database deployment from development to production.
+-----------------------------------------------------------------------------+
| DEVELOPER FLOW |
| |
| 1. Dev commits PostgresInstance Claim (YAML) to Application Git Repo |
| 2. GitOps Controller (ArgoCD) detects change and applies to Control Plane |
+------------------------------------+----------------------------------------+
|
v
+------------------------------------+----------------------------------------+
| CONTROL PLANE ENGINE |
| |
| 3. OPA Gatekeeper validates security parameters (Encryption, Multi-AZ) |
| 4. Crossplane matches Claim to AWS Postgres Composition |
| 5. Crossplane provisions encrypted RDS instance via OIDC Role |
+------------------------------------+----------------------------------------+
|
v
+------------------------------------+----------------------------------------+
| MONITORING & FINOPS |
| |
| 6. Database connection secret is written back to developer namespace |
| 7. CloudAtler scans resource for drift, cost-efficiency, and compliance |
+-----------------------------------------------------------------------------+
Step 1: The Developer's Claim
The application developer does not need to know Terraform syntax, state management, or AWS IAM. They simply write a PostgresInstance claim and place it inside their application's Git repository alongside their deployment manifests.
# app-db-claim.yaml
apiVersion: database.cloudatler.com/v1alpha1
kind: PostgresInstance
metadata:
name: payment-service-db
namespace: payment-prod
spec:
parameters:
storageGB: 100
version: "14"
writeConnectionSecretToRef:
name: db-conn-credentials
Step 2: GitOps Reconciliation
A GitOps operator (such as ArgoCD) watches the repository and applies the manifest to the EKS control plane cluster. The Crossplane controller detects the new claim, matches it against the active CompositePostgresInstance XRD, and selects the production AWS Composition.
Step 3: Secret Injection and Application Consumption
Once the RDS database is provisioned in AWS, Crossplane retrieves the endpoint, username, and auto-generated password from the cloud provider and writes them directly into a Kubernetes Secret named db-conn-credentials in the developer's namespace. The application deployment can then mount this secret as environment variables, completing the self-service loop securely.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: payment-prod
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: payment-service:v2.1.0
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-conn-credentials
key: endpoint
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-conn-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-conn-credentials
key: password
Operational Trade-offs: When to Use Which?
While the combination of Terraform and Crossplane provides an incredibly robust architecture, engineers must evaluate the operational trade-offs before implementing this topology at scale.
The Case for Pure Terraform
If your organization operates primarily in a single cloud provider, has a relatively stable infrastructure footprint, and does not run Kubernetes, adopting Crossplane introduces unnecessary complexity. Running a highly available Kubernetes cluster solely to act as a Crossplane control plane introduces management overhead, compute costs, and a steep learning curve for teams unfamiliar with Kubernetes manifests, CRDs, and debugging controller logs.
The Case for Pure Crossplane
If your organization is heavily invested in Kubernetes, operates a multi-tenant platform-as-a-service (PaaS) for internal developers, and struggles with configuration drift, Crossplane is the superior choice. It completely eliminates the "pipeline tax"—the practice of running hundreds of Terraform CI/CD pipelines daily just to execute minor configuration changes—by replacing it with a centralized, highly efficient control loop.
The Hybrid Ideal
For mature enterprise environments, the hybrid approach represents the gold standard. By utilizing Terraform to build the immutable foundations and Crossplane to manage the mutable, developer-facing application dependencies, organizations achieve the perfect balance of rigid security governance and agile developer self-service.
Unifying Multi-Cloud Operations with CloudAtler
As you scale your declarative infrastructure across AWS, Azure, and GCP, managing the sheer volume of Terraform state files, Kubernetes control planes, and Crossplane compositions can introduce operational fragmentation. Security alerts become siloed, and cloud spend can easily spiral out of control without real-time visibility.
This is where CloudAtler transforms your operational workflow. CloudAtler acts as the intelligent orchestration layer above your infrastructure engines. By unifying FinOps, cloud security, and automated operations into a single platform, CloudAtler ensures that whether a resource is provisioned via a Terraform plan or a Crossplane reconciliation loop, it is instantly scanned for vulnerabilities, mapped to budget forecasts, and optimized for performance.
With CloudAtler, you gain:
Continuous Drift Detection & Remediation: Real-time visibility into manual modifications, with automated rollbacks or state updates to maintain compliance.
Intelligent FinOps Forecaster: Predictive analysis of your Crossplane Compositions to ensure developers don't provision resources that exceed departmental budgets.
Unified Security Dashboard: A single control center mapping vulnerability data and policy compliance across all public cloud accounts and Kubernetes clusters.
Don't let multi-cloud complexity stall your engineering velocity or inflate your cloud budget. Unify your declarative infrastructure, secure your control planes, and optimize your cloud spend with CloudAtler. Schedule a demo today to see how we can bring absolute clarity to your cloud operations.
All in One Place
Atler Pilot decodes your cloud spend story by bringing monitoring, automation, and intelligent insights together for faster and better cloud operations.

